﻿/*
 *  Author: Louis A. Leskoviansky
 *  Company: Artemis Solutions Group (http://www.artemis-solutions.com)
 */


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;


namespace AjaxControlFramework.Compilation
{
    public class JsxParser
    {
        //------// Properties \\--------------------------------------------\\
        protected JavaScriptCodeDom CodeDom = null;
        public virtual string FileName { get; set; }


        public virtual bool ParsingComplete { get; protected set; }
        public virtual string ReferencedTypeName { get; protected set; } // This will be the value of the "Inherits" attribute of the directive code block found while parsing a .jsx file. The default value is "".
        public virtual bool Singleton { get; protected set; } // This will be the value of the "Singleton" attribute of the directive code block found while parsing a .jsx file. The default value is false.
        public virtual string Language { get; protected set; } // This will be the value of the "Language" attribute of the directive code block found while parsing a .jsx file. The default value is "C#".
        public virtual string ScriptName { get; protected set; } // This will be the value of the "ScriptName" attribute.
        public virtual ScriptType ScriptType { get; protected set; } // This will be the value of the "ScriptType" attribute.


        // Fields that assist with parsing.
        protected int CurrentPosition = 0;
        protected StringBuilder CodeExpressionBuffer = null;
        protected StringBuilder CodeExpressionPrefixBuffer = null;
        protected StringBuilder DataBindingExpressionBuffer = null;
        protected StringBuilder SnippetExpressionBuffer = null;
        protected StringBuilder EvalExpressionBuffer = null;
        protected StringBuilder DirectiveBuffer = null;
        protected StringBuilder LiteralBuffer = null;
        protected bool InsideCodeBlock = false;
        protected bool InsideLiteral = false;
        protected bool AtCodeBlockStartTag = false;
        protected bool InsideDirective = false;
        protected bool InsideCodeExpression = false;
        protected bool InsideSnippetExpression = false;
        protected bool InsideEvalExpression = false;
        protected bool InsideDataBindingExpression = false;
        protected bool AtCodeExpressionPrefix = false;
        protected bool InsideStringLiteral = false;
        //------\\ Properties //--------------------------------------------//



        //------// Constructors \\------------------------------------------\\
        public JsxParser()
        { }
        //------\\ Constructors //------------------------------------------//



        //------// Methods \\-----------------------------------------------\\
        public virtual JavaScriptCodeDom ParseFile(TextReader reader, string fileName)
        {
            FileName = fileName;

            // Parse through each line of the source code file individually and build out 
            // a JavaScriptCodeDom object.
            CodeDom = new JavaScriptCodeDom();

            string currentLine = null;
            int lineNumberCount = 1;

            while ((currentLine = reader.ReadLine()) != null)
            {
                ParseLine(currentLine, lineNumberCount);
                lineNumberCount++;
            }

            if (InsideLiteral && LiteralBuffer.Length > 0)
            {
                CodeDom.AddSourceElement(lineNumberCount, new JavaScriptSourceElement(LiteralBuffer.ToString(), JavaScriptSourceElementType.Literal));
            }

            CodeDom.AddSourceElement(lineNumberCount + 1, new JavaScriptSourceElement(String.Empty, JavaScriptSourceElementType.EndOfFile));


            CodeDom.InheritedTypeName = ReferencedTypeName;
            CodeDom.Language = Language;
            CodeDom.ScriptName = ScriptName;
            CodeDom.ScriptType = ScriptType;
            CodeDom.Singleton = Singleton;

            ParsingComplete = true;

            return CodeDom;
        }



        protected virtual void ParseLine(string line, int lineNumber)
        {
            CurrentPosition = 0;

            char currentChar = ' ';
            char prevChar = ' ';
            char nextChar = ' ';


            if (line.Length == 0)
            {
                line = Environment.NewLine; // Empty lines of the source file should be parsed as New Line strings.

                currentChar = line[0];
                nextChar = line[1];

                // 1.) Process the first character.
                ParseCharacter(prevChar, currentChar, nextChar, lineNumber);

                CurrentPosition++;

                prevChar = line[0];
                currentChar = line[1];
                nextChar = ' ';

                // 2.) Process the second and last character.
                ParseCharacter(prevChar, currentChar, nextChar, lineNumber);

                return;
            }


            if (line.Length >= 2)
            {
                currentChar = line[0];
                nextChar = line[1];

                // 1.) Process the first character.
                ParseCharacter(prevChar, currentChar, nextChar, lineNumber);

                
                if (line.Length == 2 && CurrentPosition == 0)
                {
                    prevChar = currentChar;
                    currentChar = nextChar;
                    nextChar = ' ';

                    // 2.) Process the second and last character.
                    ParseCharacter(prevChar, currentChar, nextChar, lineNumber);

                    if (InsideLiteral && LiteralBuffer.Length > 0)
                    {
                        AppendLiteralCharacter(Environment.NewLine[0]);
                        AppendLiteralCharacter(Environment.NewLine[1]);
                    }

                    return;
                }
            }
            else if (line.Length == 1)
            {
                currentChar = line[0];
                nextChar = ' ';

                // 1.) Process the first (and last) character.
                ParseCharacter(prevChar, currentChar, nextChar, lineNumber);

                if (InsideLiteral && LiteralBuffer.Length > 0)
                {
                    AppendLiteralCharacter(Environment.NewLine[0]);
                    AppendLiteralCharacter(Environment.NewLine[1]);
                }

                return;
            }


            // 2.) Process the remaining characters.
            while (line.Length - (CurrentPosition + 2) > 0)
            {
                CurrentPosition++;

                prevChar = line[CurrentPosition - 1];
                currentChar = line[CurrentPosition];
                nextChar = line[CurrentPosition + 1];

                ParseCharacter(prevChar, currentChar, nextChar, lineNumber);
            }
            

            // 3.) Process the last character.
            CurrentPosition++;

            if (CurrentPosition < line.Length)
            {
                prevChar = line[CurrentPosition - 1];
                currentChar = line[CurrentPosition];
                nextChar = ' ';

                ParseCharacter(prevChar, currentChar, nextChar, lineNumber);

                if (InsideLiteral && LiteralBuffer.Length > 0)
                {
                    AppendLiteralCharacter(Environment.NewLine[0]);
                    AppendLiteralCharacter(Environment.NewLine[1]);
                }
            }
        }


        protected virtual void ParseCharacter(char prevChar, char currentChar, char nextChar, int lineNumber)
        {
            // The parser should be looking for code blocks (start with "<%" and end with "%>").
            
            // "<%@" strings signal the start of a code directive. The only accepted directive in a .jsx 
            // file is "<%@ Script Inherits="" Language="C#|VB" Singleton="true|false" ScriptName="" AutoRender="true|false" RenderMode="Inline|Resource|File" RenderLocation="Head|Body|BeforeContent|AfterContent" %>".


            if (InsideCodeBlock)
            {
                if (AtCodeBlockStartTag)
                {
                    if (currentChar == '@') // Directive - "<%@"
                    {
                        InsideDirective = true;
                        DirectiveBuffer = new StringBuilder(100);
                    }
                    else if (currentChar == '=') // SnippetExpression - "<%="
                    {
                        InsideEvalExpression = true;
                        EvalExpressionBuffer = new StringBuilder(100);
                    }
                    else if (currentChar == '#') // DataBindingExpression - "<%#"
                    {
                        InsideDataBindingExpression = true;
                        DataBindingExpressionBuffer = new StringBuilder(100);
                    }
                    else if (currentChar == '$') // CodeExpression - "<%$"
                    {
                        InsideCodeExpression = true;
                        AtCodeExpressionPrefix = true;

                        CodeExpressionBuffer = new StringBuilder(100);
                        CodeExpressionPrefixBuffer = new StringBuilder(20);
                    }
                    else // SnippetExpression - "<%"
                    {
                        InsideSnippetExpression = true;
                        SnippetExpressionBuffer = new StringBuilder(100);

                        SnippetExpressionBuffer.Append(currentChar); // This is the only state where the current character can be process while at the start tag of the code block.
                    }

                    AtCodeBlockStartTag = false;
                    return;
                }
                else
                {
                    if (InsideCodeExpression)
                    {
                        if (AtCodeExpressionPrefix)
                        {
                            // Process the current character.
                            if (currentChar == ':')
                            {
                                AtCodeExpressionPrefix = false;
                            }
                            else
                            {
                                CodeExpressionPrefixBuffer.Append(currentChar);

                                // Process the next character.
                                if (nextChar == ':')
                                {
                                    AtCodeExpressionPrefix = false;
                                    CurrentPosition++;
                                }
                            }
                        }
                        else if (currentChar != '%')
                        {
                            CodeExpressionBuffer.Append(currentChar);
                        }
                    }
                    else if (InsideDataBindingExpression && currentChar != '%')
                    {
                        // DataBinding Expressions are a special case of Code Expressions. They are recognized within ASP.NET as 
                        // Code Expressions with empty prefixes.

                        DataBindingExpressionBuffer.Append(currentChar);
                    }
                    else if (InsideSnippetExpression || InsideEvalExpression)
                    {
                        if (!InsideStringLiteral && (currentChar == '%' || currentChar == '>'))
                        {
                            // Don't append the current character.
                        }
                        else
                        {
                            if (InsideEvalExpression)
                            {
                                EvalExpressionBuffer.Append(currentChar);
                            }
                            else
                            {
                                SnippetExpressionBuffer.Append(currentChar);
                            }
                        }
                    }
                    else if (InsideDirective)
                    {
                        DirectiveBuffer.Append(currentChar);
                    }
                    else
                    {
                        // TODO: Create a compile error.
                    }
                }



                // TODO: Check for String Literals ('"').


                if (!InsideStringLiteral)
                {
                    if ((currentChar == '%' && nextChar == '>') || (prevChar == '%' && currentChar == '>'))
                    {
                        if (AtCodeExpressionPrefix)
                        {
                            // TODO: Generate a compilation error.

                            AtCodeExpressionPrefix = false;
                        }


                        // We're at the end of the code block so we can turn off all of the flags for recognizing Code Block components, 
                        // and create an object in the DOM structure for the document.
                        if (InsideCodeExpression)
                        {
                            CodeDom.AddSourceElement(lineNumber, new JavaScriptSourceElement(CodeExpressionBuffer.ToString().Trim(), JavaScriptSourceElementType.CodeExpression, CodeExpressionPrefixBuffer.ToString().Trim()));
                            InsideCodeExpression = false;
                        }
                        else if (InsideDataBindingExpression)
                        {
                            CodeDom.AddSourceElement(lineNumber, new JavaScriptSourceElement(DataBindingExpressionBuffer.ToString().Trim(), JavaScriptSourceElementType.DataBindingExpression));
                            InsideDataBindingExpression = false;
                        }
                        else if (InsideDirective)
                        {
                            InsideDirective = false;

                            string[] directiveSplit = DirectiveBuffer.ToString().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

                            // Both the "Inherits" and "Language" attributes are REQUIRED.
                            bool foundInheritsAttribute = false;
                            bool foundSingletonAttribute = false;
                            bool foundLanguageAttribute = false;
                            bool foundScriptNameAttribute = false;
                            bool foundScriptTypeAttribute = false;

                            string inheritsAttributeValue = String.Empty;
                            string languageAttributeValue = String.Empty;
                            string singletonAttributeValue = String.Empty;
                            string scriptNameAttributeValue = String.Empty;
                            string scriptTypeAttributeValue = String.Empty;

                            for (int index = 1; index < directiveSplit.Length; index++)
                            {
                                string[] attributeSplit = directiveSplit[index].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);

                                if (attributeSplit == null || attributeSplit.Length == 0) { continue; }

                                switch (attributeSplit[0].Trim().ToLower())
                                {
                                    case "inherits":

                                        foundInheritsAttribute = true;
                                        inheritsAttributeValue = attributeSplit[1].Substring(1, attributeSplit[1].Length - 2).Trim();
                                        break;

                                    case "singleton":

                                        foundSingletonAttribute = true;
                                        singletonAttributeValue = attributeSplit[1].Substring(1, attributeSplit[1].Length - 2).Trim().ToLower();

                                        // Only "true" or "false" is valid for the "Singleton" attribute of the "Script" directive.
                                        if (singletonAttributeValue != "true" && singletonAttributeValue != "false")
                                        {
                                            // TODO: Create a compile error.
                                            singletonAttributeValue = null;
                                        }
                                        break;

                                    case "language":

                                        foundLanguageAttribute = true;
                                        languageAttributeValue = attributeSplit[1].Substring(1, attributeSplit[1].Length - 2).Trim().ToLower();

                                        // Only values of "C#" and "VB" are allowed.
                                        if (languageAttributeValue != "c#" && languageAttributeValue != "vb")
                                        {
                                            // TODO: Create a compile error.
                                            languageAttributeValue = null;
                                        }

                                        break;

                                    case "scriptname":

                                        foundScriptNameAttribute = true;
                                        scriptNameAttributeValue = attributeSplit[1].Substring(1, attributeSplit[1].Length - 2).Trim();

                                        // Value of the "ScriptName" attribute cannot be empty when the attribute is specified.
                                        if (scriptNameAttributeValue == String.Empty)
                                        {
                                            // TODO: Create a compile error.
                                        }

                                        break;

                                    case "scripttype":

                                        foundScriptTypeAttribute = true;
                                        scriptTypeAttributeValue = attributeSplit[1].Substring(1, attributeSplit[1].Length - 2).Trim().ToLower();

                                        bool scriptTypeExists = false;
                                        foreach (string scriptTypeName in Enum.GetNames(typeof(ScriptType)))
                                        {
                                            if (scriptTypeName.ToLower() == scriptTypeAttributeValue)
                                            {
                                                scriptTypeExists = true;
                                                break;
                                            }
                                        }


                                        if (scriptTypeAttributeValue == String.Empty)
                                        {
                                            // TODO: Generate a compiler error.

                                        }
                                        // The value provided for the "RenderMode" attribute is not valid.
                                        else if (!scriptTypeExists)
                                        {
                                            // TODO: Generate a compiler error.
                                            scriptTypeAttributeValue = null;
                                        }

                                        break;

                                    default:

                                        continue;
                                }
                            }


                            if (!foundInheritsAttribute || !foundLanguageAttribute)
                            {
                                // TODO: Create a compile error. The "Inherits" and "Language" attributes is required within the "Script" directive.
                            }
                            else
                            {
                                // Process the other rules of the "Script" directive.                         


                                // Set the Inherits attribute of the JavascriptCodeDom.
                                ReferencedTypeName = inheritsAttributeValue;


                                // Set the Language attribute of the JavascriptCodeDom.
                                Language = languageAttributeValue;


                                // Set the Singleton attribute of the JavascriptCodeDom.
                                if (!foundSingletonAttribute)
                                {
                                    Singleton = false;
                                }
                                else
                                {
                                    bool singleton = false;
                                    Boolean.TryParse(singletonAttributeValue, out singleton);
                                    Singleton = singleton;
                                }


                                // Set the ScriptName attribute of the JavascriptCodeDom.
                                if (!foundScriptNameAttribute)
                                {
                                    ScriptName = Path.GetFileNameWithoutExtension(FileName);
                                }
                                else
                                {
                                    ScriptName = scriptNameAttributeValue;
                                }


                                // Set the RenderMode attribute of the JavascriptCodeDom.
                                if (String.IsNullOrEmpty(scriptTypeAttributeValue))
                                {
                                    ScriptType = ScriptType.Inline;
                                }
                                else
                                {
                                    ScriptType = (ScriptType)Enum.Parse(typeof(ScriptType), scriptTypeAttributeValue, true);
                                }
                            }
                        }
                        else if (InsideSnippetExpression)
                        {
                            InsideSnippetExpression = false;
                            CodeDom.AddSourceElement(lineNumber, new JavaScriptSourceElement(SnippetExpressionBuffer.ToString().Trim(), JavaScriptSourceElementType.SnippetExpression));
                        }
                        else if (InsideEvalExpression)
                        {
                            InsideEvalExpression = false;
                            CodeDom.AddSourceElement(lineNumber, new JavaScriptSourceElement(EvalExpressionBuffer.ToString().Trim(), JavaScriptSourceElementType.EvalExpression));
                        }


                        if (currentChar == '%' && nextChar == '>')
                        {
                            CurrentPosition++; // Move the CurrentPosition pointer forward a character since the nextChar is being procesed.
                        }

                        InsideCodeBlock = false; // Take the parser out of the context of a Code Block.
                    }
                }                
            }
            else
            {
                if (currentChar == '<' && nextChar == '%')
                {
                    CurrentPosition++;
                    InsideCodeBlock = true;
                    AtCodeBlockStartTag = true;

                    if (InsideLiteral)
                    {
                        InsideLiteral = false;
                        CodeDom.AddSourceElement(lineNumber, new JavaScriptSourceElement(LiteralBuffer.ToString(), JavaScriptSourceElementType.Literal));
                        LiteralBuffer = new StringBuilder(100);
                    }
                }
                else if (prevChar == '<' && currentChar == '%')
                {
                    InsideCodeBlock = true;
                    AtCodeBlockStartTag = true;

                    if (InsideLiteral)
                    {
                        InsideLiteral = false;
                        CodeDom.AddSourceElement(lineNumber, new JavaScriptSourceElement(LiteralBuffer.ToString(), JavaScriptSourceElementType.Literal));
                        LiteralBuffer = new StringBuilder(100);
                    }
                }
                else
                {
                    if (!InsideLiteral)
                    {
                        InsideLiteral = true;

                        if (LiteralBuffer == null)
                        {
                            LiteralBuffer = new StringBuilder(100);
                        }
                    }

                    AppendLiteralCharacter(currentChar);
                }
            }
        }


        protected virtual void AppendLiteralCharacter(char character)
        {
            // If the character is special and would normally require to be escaped in source code, escape it before 
            // appending it to the literal buffer. These are special characters: \n, \t, \r, \v, \f, and ".

            if (character == '\n')
            {
                LiteralBuffer.Append("\\n");
            }
            else if (character == '\t')
            {
                LiteralBuffer.Append("\\t");
            }
            else if (character == '\r')
            {
                LiteralBuffer.Append("\\r");
            }
            else if (character == '\v')
            {
                LiteralBuffer.Append("\\v");
            }
            else if (character == '\f')
            {
                LiteralBuffer.Append("\\f");
            }
            else if (character == '"')
            {
                LiteralBuffer.Append("\\\"");
            }
            else
            {
                LiteralBuffer.Append(character);
            }
        }



        public virtual Type GetReferencedType()
        {
            if (ReferencedTypeName == null) { return null; }

            Type referencedType = null;
            referencedType = Type.GetType(ReferencedTypeName, false);

            return referencedType;
        }
        //------\\ Methods //-----------------------------------------------//
    }
}